
package horde.patterns.master;

import horde.comunications.RMIDock;
import horde.interfaces.Requests;
import horde.patterns.Agent;
import horde.patterns.Msg;
import horde.internalResources.dictionaries.dictionary;
import horde.internalResources.Directory;
import horde.internalResources.HorrorControl.HorrorControl;
import horde.utils.RawLoader;
import horde.utils.Toolbox;
import java.awt.AWTException;
import java.awt.Image;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import java.util.Properties;
import java.util.Set;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.Timer;

/**
 * Implements the master agent tasked with the node control, and manages the agents
 * running in the system.
 * @author Arturo Mazón
 * @author Andres Picazo
 */

public class MasterAgent {

    // <editor-fold defaultstate="collapsed" desc="Logic">
    public MasterAgent (String id) {
        init (id, null);
    }

    public MasterAgent (String id, Properties p) {
        init(id, p);
    }

    /**
     * Initializes the master
     * @param id The master's name
     */
    private void init (String id, Properties p) {
//        dictionary.setBundle(language);
        this.id = id;
        this.directory = Directory.getInstance();
        hc = HorrorControl.getInstance();
        this.requests = Requests.createRequests(this);
        this.directory.setRequests(requests);
        this.trayIcon = null;
        this.createTrayIcon();
        masterPanel = new MasterPanel();
        
        setProperties (p);
        if (p != null) {
            String s = directory.getMasterProperty("remote");
            if (s != null && s.toLowerCase().equals("true")) {
                trace ("Starting RMI...");
                RMIDock.getInstance().startRMI();
                trace ("Registering master as " + RMIDock.defaultMasterID);
                RMIDock.getInstance().registerObject(RMIDock.defaultMasterID, requests);
            } else trace ("RMI not active");
        }

        localIP = new Toolbox().getIP();
    }

    /**
     * Ask for the master's name
     * @return The name of the master
     */
    public String getID () { return this.id; }

    /**
     * Sets the general properties for the application, saved into the directory
     * @param p The set of properties
     */
    public void setProperties (Properties p) {
        this.directory.setMasterProperties(p);
    }

    /**
     * Read a single application property
     * @param propertyName The name of the property
     * @return The value of the property, or null if nothing has been found
     */
    public String getProperty (String propertyName) {
        return this.directory.getMasterProperty(propertyName);
    }

    /**
     * Sets the table of routes to find other nodes, given as pairs [id,ip]
     * @param routes The table of routes, as a properties object
     */
    public void setRoutes (Properties routes) {
        this.directory.setRoutes(routes);
    }

    /**
     * Adds a new agent to the master. Assigns a requests clearance for the master
     * and creates a message box for the agent. The agent will have no properties.
     * @param agent The agent to be added to the agent pool
     * @return True if the agent has been added, false if the agent was already registered
     */
    public boolean addAgent (Agent agent) {
        if (directory.containsAgentID(agent.getID())) return false;
        else {
            agent.setRequests(requests);
            directory.addAgent(agent, null);
            agent.setMessageBox(directory.getAgentBox(agent.getID()));
            hc.addAgentToVis(agent.getID());
            return true;
        }
    }

    /**
     * Adds a new agent to the master. Assigns a requests clearance for the master
     * and creates a message box for the agent.
     * @param agent The agent to be added to the agent pool
     * @param properties The agent properties
     * @return True if the agent has been added, false if the agent was already registered
     */
    public boolean addAgent (Agent agent, Properties properties) {
        if (directory.containsAgentID(agent.getID())) return false;
        else {
            agent.setRequests(requests);
            directory.addAgent(agent, properties);
            agent.setMessageBox(directory.getAgentBox(agent.getID()));
            hc.addAgentToVis(agent.getID());
            return true;
        }
    }

    /**
     * Adds a new shared resource to the directory
     * @param id The identification to find the resource later
     * @param type Usually the path to instance the object, like: horde.minefield.andres.robots.robotControl
     * @param resource The already created new shared resource
     */
    public void addSharedResource (String id, String type, Object resource) {
        directory.addSharedResource(id,type,resource);
    }

    /**
     * Get a shared resource from the directory
     * @param id The name of the requested resource
     * @return The resource associated with the id, if any. Otherwise returns null
     */
    public Object getSharedResource (String id) {
        return directory.getSharedResource(id);
    }

    /**
     * This method stops and removes an agent, note that this might be dangerous, as it stops the agent
     * without letting it finish wathever work it was doing
     * @param agentID
     * @return True if the agent was registered, otherwise returns false, no agent found.
     */

    public boolean forceRemoveAgent (String agentID) {
        if (directory.containsAgentID(agentID)) {
            Thread agentThread = directory.getAgentThread(agentID);
            if (agentThread != null) {
                agentThread.stop();
                directory.removeAgent(agentID);
                return true;
            }
            else return false;
        } else return false;
    }

    /**
     * Invokes the method start for the threads of the registered agents
     * @return True if there was at least one agent to start, false otherwise
     */
    public boolean startAgents () {
        if (directory.getSize() != 0) {
            Set keys = directory.getKeys();
            Iterator<String> i = keys.iterator();
            while (i.hasNext()) {
                String agent = i.next();
                Thread t = directory.getAgentThread(agent);
                trace ("Starting " + agent);
                t.start();
            }
            return true;
        } else return false;
    }

    /**
     * Sends the agents a control message requesting them to stop, when there is no
     * more agents running, the master will stop
     */
    public void stopAgents () {
        /* First, close rmi to prevent remote messages to keep arriving */
        RMIDock.getInstance().removeAllObjects();
        /* Stop the agents */
        if (directory.getSize() != 0) {
            Set keys = directory.getKeys();
            Object [] key_list = keys.toArray();
            for (int i = 0; i < key_list.length; i++) {                
                directory.putMessage(Msg.stop_request(id, (String)key_list[i]));
            }
        }
        
        /* Master panel should be closed too? */
        if (masterPanel != null) {
            int answer = JOptionPane.showConfirmDialog(masterPanel, "¿Close the master panel?", "Attention", JOptionPane.YES_NO_OPTION);
            if (answer == JOptionPane.YES_OPTION) {
                masterPanel.setVisible(false);                
                System.out.println ("Waiting for agents to stop...");
                /* Wait until all threads are stopped */
                Timer t = new Timer(1000, new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        if (getNumberOfAgentsRunning() == 0) {
                            masterPanel.dispose();
                            if (trayIcon != null) SystemTray.getSystemTray().remove(trayIcon);
                            System.exit(0);
                        }
                    }
                });
                t.setRepeats(true);
                t.start();
//                while (getNumberOfAgentsRunning() != 0) {
//                    /* Change this with a timer, with a sleep i can't force stop while waiting */
//                    try { Thread.sleep(1000); }
//                    catch (Exception e) {}
//                }
//                masterPanel.dispose();
//                /* Closes the system. Required because requests is now a unicastRemoteObject */
//                System.exit(0);
            }
            else {
                masterPanel.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                masterPanel.big();
                masterPanel.setVisible(true);
                /* Remove tray icon last, so the option to force close is available until the end */
                if (trayIcon != null) SystemTray.getSystemTray().remove(trayIcon);
            }
        }
        
    }

    /**
     * Gets the number of agents existing as threads in the master agent record
     * @return Number of agent threads
     */
    public int getNumberOfAgentsRunning () {
        if (directory.getSize() != 0) {
            int count = 0;
            Set keys = directory.getKeys();
            Object [] key_list = keys.toArray();
            for (int i = 0; i < key_list.length; i++) {
                Thread t = directory.getAgentThread((String)key_list[i]);
                if (t.isAlive()) count++;
            }
            return count;
        } else return 0;
    }

    /**
     * Simple implementation for sending messages
     * @param msg The message to be delivered
     * @return True if the message was delivered
     */
    @Deprecated
    public boolean sendMessage (Msg msg) {
        /* The message destination is local? */
        if (directory.containsAgentID(msg.getDestination())) {
            return directory.putMessage(msg);
        } else return false;
    }

    /**
     * Post a new message, taking into account the type of message, like groups
     * @param msg The message to be posted
     * @return True if the message was successfully posted, in case of a group, if at least one
     * has received the message.
     */
    public boolean post (Msg msg) {
        boolean result = false;
        if (msg.getIpSource().equals(localIP)) {
            trace (msg.getSource(), msg.getTrunk()[0].toString(), msg.getFormattedCreationTime());
        } else {
            trace ("Received remote message from " + msg.getIpSource() + " \n" + msg.getTrunk()[0].toString());
        }
        // <editor-fold defaultstate="collapsed" desc="Group posting">
        /* Check first if destination is a group of agents */
        /* Group returns true if anyone has received the message */
        if (msg.getControl() == Msg.DESTINATION_IS_A_GROUP) {            
            ArrayList<String> groupMembers = directory.getGroupMembers(msg.getDestination());
            if (groupMembers != null && !groupMembers.isEmpty()) {
                for (String member : groupMembers) {
                    /* Send to a single recipient a copy of the messsage */
                    Msg clone = Msg.clone(msg);
                    clone.setDestination(member);
                    clone.setControl(Msg.STANDARD);
                    /* Don't send the message to the source! */
                    if (!member.equals(clone.getSource())) {
                        boolean messageDelivered = directory.putMessage(clone);
                        /* Recipient is not local, sent to remote */
                        //Hay que actualizar el message delivered con el postRemote
                        if (!messageDelivered) messageDelivered=postRemote(clone);
                        result = result || messageDelivered;
//                        result = result && directory.putMessage(clone);
                    }
                }
            } else result = false;
        }
        // </editor-fold>
        // <editor-fold defaultstate="collapsed" desc="Standard posting">
        else if (msg.getControl() == Msg.STANDARD) {
            result = directory.putMessage(msg);
            if (!result) result = postRemote (msg);
        }
        // </editor-fold>
        return result;
    }

    /**
     * If remote is enabled for this master, try to post a remote message
     * @param msg Message to be posted
     * @return True if the message was sent, false otherwise
     */
    private boolean postRemote (Msg msg) {
        String s = directory.getMasterProperty("remote");
        if (s.toLowerCase().equals("true")) return RMIDock.getInstance().post(msg);
        else return false;
    }

    /**
     * Sends a new info trace to the traces panel in the HorrorControl
     * @param src Source of the trace
     * @param text Message of the trace
     * @param date Timestamp for the trace
     */
    public void trace (String src, String text, String date) {
        HorrorControl.getInstance().trace(src, text, HorrorControl.TRACE_INFO, date);
    }

    public void trace (String text) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String date = sdf.format(Calendar.getInstance().getTime());
        HorrorControl.getInstance().trace(id, text, HorrorControl.TRACE_INFO, date);
    }
    
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Graphics">
    /**
     * Creates the tray icon which supports the master panel, containing the traces and the
     * manual exit option
     */
    private void createTrayIcon () {
        if (SystemTray.isSupported()) {
            try {
                PopupMenu trayMenu = new PopupMenu();
                MenuItem exit = new MenuItem(dictionary.getString("master.tray.option.exit"));
                MenuItem small = new MenuItem("Small");
                MenuItem big = new MenuItem("Big");
                MenuItem forceClose = new MenuItem(dictionary.getString("master.tray.option.forceClose"));
                MenuItem saveTraces = new MenuItem(dictionary.getString("master.tray.option.saveTraces"));
                trayMenu.add (small);
                trayMenu.add (big);
                trayMenu.add (exit);
                trayMenu.add (forceClose);
                
                trayMenu.addSeparator();
                trayMenu.add(saveTraces);

                exit.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        SystemTray.getSystemTray().remove(trayIcon);
                        stopAgents();
                    }
                });

                small.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        masterPanel.small();
                    }
                });

                big.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        masterPanel.big();
                    }
                });

                forceClose.addActionListener(new ActionListener () {

                    public void actionPerformed(ActionEvent e) {
                        System.exit(0);
                    }
                });
                saveTraces.addActionListener(new ActionListener() {
                    public void actionPerformed(ActionEvent e) {
                        HorrorControl.getInstance().export("File");
                    }
                });
                Image img = RawLoader.getRawImage(RawLoader.hordeIcon).getImage();
                trayIcon = new TrayIcon(img);
                trayIcon.setToolTip("Horde Master " + this.id + " control");
                trayIcon.setPopupMenu(trayMenu);
                trayIcon.addMouseListener(new MouseControl());
                trayIcon.setToolTip("Local ip: " + (new Toolbox()).getIP());
                try {
                    SystemTray.getSystemTray().add(trayIcon);
                } catch (AWTException ex) {                    
                    System.err.println ("Fallo al iniciar bandeja");
                }
            }
            catch (Exception e1) {
                System.err.println ("Fallo al inicializar icono de la bandeja\n"+
                        e1.getMessage());
            }
        } else System.err.println ("No tray available");
    }

    private class MouseControl implements MouseListener {

        public void mouseClicked(MouseEvent e) {
            if (e.getButton() == MouseEvent.BUTTON1) {
                if (e.getClickCount() > 1){
                    masterPanel.swapSize();
                }
                else masterPanel.swapVisibility();
            }

        }

        public void mousePressed(MouseEvent e) {}
        public void mouseReleased(MouseEvent e) {}
        public void mouseEntered(MouseEvent e) {}
        public void mouseExited(MouseEvent e) {}

    }
    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc="Attributes">

    /**
     * The master identificator
     */
    private String id;
    /**
     * Node directory, to be used by the master
     */
    private Directory directory;
    /**
     * A link for the agents to be able to access some of the master's methods, but not all
     */
    private Requests requests;
    /**
     * Container for the system tray icon, which displays the horde logo
     */
    private TrayIcon trayIcon;
    /**
     * Window (JPanel) shown when the icon in the system tray is clicked
     */
    private MasterPanel masterPanel;
    /**
     * Link to the horrorControl, to process traces and debug options
     */
    private HorrorControl hc;

    private String localIP;

    // </editor-fold>
}
